﻿using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Mono.Cecil;
using WeavingCommon;

namespace NotifyPropertyWeaverMsBuildTask
{
    [Export, PartCreationPolicy(CreationPolicy.Shared)]
    public class MethodFinder
    {
        MethodGenerifier methodGenerifier;
        MethodInjector methodInjector;
        TypeNodeBuilder typeNodeBuilder;
        WeavingTask weavingTask;
        ModuleReader moduleReader;
        Logger logger;
        TypeResolver typeResolver;
        string eventInvokerName = "OnPropertyChanged";
        Dictionary<string, string> supportedFrameworksMapping; 

        [ImportingConstructor]
        public MethodFinder(MethodGenerifier methodGenerifier, MethodInjector methodInjector, TypeNodeBuilder typeNodeBuilder, WeavingTask weavingTask, ModuleReader moduleReader, Logger logger, TypeResolver typeResolver)
        {
            this.methodGenerifier = methodGenerifier;
            this.methodInjector = methodInjector;
            this.typeNodeBuilder = typeNodeBuilder;
            this.weavingTask = weavingTask;
            this.moduleReader = moduleReader;
            this.logger = logger;
            this.typeResolver = typeResolver;

            supportedFrameworksMapping = new Dictionary<string, string>
                                             {
                                                 {"Caliburn.Micro.PropertyChangedBase", "NotifyOfPropertyChange"},
                                                 {"Caliburn.PresentationFramework.PropertyChangedBase", "NotifyOfPropertyChange"},
                                                 {"GalaSoft.MvvmLight.ObservableObject", "RaisePropertyChanged"},
                                                 {"GalaSoft.MvvmLight.ViewModelBase", "RaisePropertyChanged"},
                                                 {"Microsoft.Practices.Prism.ViewModel.NotificationObject", "RaisePropertyChanged"},
                                                 {"Cinch.ViewModelBase", "NotifyPropertyChanged"},
                                                 {"Magellan.Framework.PresentationObject", "NotifyChanged"},
                                                 {"Catel.Data.ObservableObject", "RaisePropertyChanged"},
                                                 //Cant support nrount because it does not have a string based notify
                                                 //{"nRoute.ViewModels.ViewModelBase", "NotifyPropertyChanged"},
                                             };
        }


        void ProcessChildNode(TypeNode node, EventInvokerMethod eventInvoker)
        {
            var childEventInvoker = FindEventInvokerMethod(node.TypeDefinition);
            if (childEventInvoker == null)
            {
                if (node.TypeDefinition.BaseType.IsGenericInstance)
                {
                    var methodReference = MethodGenerifier.MakeGeneric(node.TypeDefinition.BaseType, eventInvoker.MethodReference);
                    eventInvoker = new EventInvokerMethod
                                       {
                                           IsBeforeAfter = eventInvoker.IsBeforeAfter,
                                           MethodReference = methodReference,
                                           IsVirtual = eventInvoker.IsVirtual
                                       };
                }
            }
            else
            {
                eventInvoker = childEventInvoker;
            }

            node.EventInvoker = eventInvoker;

            foreach (var childNode in node.Nodes)
            {
                ProcessChildNode(childNode, eventInvoker);
            }
        }



        public EventInvokerMethod RecursiveFindMethod(TypeDefinition typeDefinition)
        {
            var typeDefinitions = new Stack<TypeDefinition>();
            MethodDefinition methodDefinition;
            var currentTypeDefinition = typeDefinition;
            do
            {
                typeDefinitions.Push(currentTypeDefinition);
                if (GetFrameworkMethod(currentTypeDefinition, out methodDefinition))
                {
                    break;
                }
                if (FindEventInvokerMethodDefinition(currentTypeDefinition, out methodDefinition))
                {
                    break;
                }
                var baseType = currentTypeDefinition.BaseType;

                if (baseType == null || baseType.FullName == "System.Object")
                {
                    return null;
                }
                currentTypeDefinition = typeResolver.Resolve(baseType);
            } while (true);

            return new EventInvokerMethod
                       {
                           MethodReference = methodGenerifier.GetMethodReference(typeDefinitions, methodDefinition),
                           IsBeforeAfter = IsBeforeAfterMethod(methodDefinition),
                           IsVirtual = methodDefinition.IsVirtual
                       };
        }

        bool GetFrameworkMethod(TypeDefinition currentTypeDefinition, out MethodDefinition methodDefinition)
        {
            string methodName;
            if (supportedFrameworksMapping.TryGetValue(currentTypeDefinition.FullName, out methodName))
            {
                var frameworkMethodDefinition = currentTypeDefinition.Methods.FirstOrDefault(x => x.Name == methodName && IsSingleStringMethod(x));
                if (frameworkMethodDefinition == null)
                {
                    var message = string.Format("Found base class '{0}' and expected to find method named '{1}' but did not. It is possible the library you are using for a base class has changed the name of its event invoker. If procesing failed please raise a bug here http://code.google.com/p/notifypropertyweaver/issues/list.", currentTypeDefinition.FullName, methodName);
                    logger.LogMessage(message);
                }
                else
                {
                    methodDefinition = frameworkMethodDefinition;
                    return true;
                }
            }
            methodDefinition = null;
            return false;
        }

        EventInvokerMethod FindEventInvokerMethod(TypeDefinition type)
        {
            MethodDefinition methodDefinition;
            if (FindEventInvokerMethodDefinition(type, out methodDefinition))
            {
                var methodReference = moduleReader.Module.Import(methodDefinition);
                return new EventInvokerMethod
                           {
                               MethodReference = methodReference.GetGeneric(),
                               IsBeforeAfter = IsBeforeAfterMethod(methodDefinition),
                               IsVirtual = methodDefinition.IsVirtual,
                           };
            }
            return null;
        }

        bool FindEventInvokerMethodDefinition(TypeDefinition type, out MethodDefinition methodDefinition)
        {
            methodDefinition = type.Methods
                .Where(x => x.Name == eventInvokerName)
                .OrderByDescending(definition => definition.Parameters.Count)
                .FirstOrDefault(x => IsBeforeAfterMethod(x) || IsSingleStringMethod(x));
            return methodDefinition != null;
        }

        public static bool IsSingleStringMethod(MethodDefinition method)
        {
            var parameters = method.Parameters;
            return parameters.Count == 1
                   && parameters[0].ParameterType.FullName == "System.String";
        }

        public static bool IsBeforeAfterMethod(MethodDefinition method)
        {
            var parameters = method.Parameters;
            return parameters.Count == 3
                   && parameters[0].ParameterType.FullName == "System.String"
                   && parameters[1].ParameterType.FullName == "System.Object"
                   && parameters[2].ParameterType.FullName == "System.Object";
        }

    
        public void Execute()
        {
            eventInvokerName = weavingTask.EventInvokerName;
            foreach (var notifyNode in typeNodeBuilder.NotifyNodes)
            {
                var eventInvoker = RecursiveFindMethod(notifyNode.TypeDefinition);
                if (eventInvoker == null)
                {
                    eventInvoker = methodInjector.AddOnPropertyChangedMethod(notifyNode.TypeDefinition);
                    if (eventInvoker == null)
                    {
                        logger.LogWarning(string.Format(
"\tCould not derive or inject '{0}' into '{1}'. It is possible you are inheriting from a base class and have not correctly set 'WeavingTask.EventInvokerName' or you are using a explicit PropertyChanged event and the event field is not visible to this instance. Please either correct 'WeavingTask.EventInvokerName' or implement your own EventInvoker on this class. No derived types will be processed. If you want to supress this message place a [DoNotNotifyAttribute] on {1}.", weavingTask.EventInvokerName, notifyNode.TypeDefinition.Name));
                        continue;
                    }
                }
               
                notifyNode.EventInvoker = eventInvoker;

                foreach (var childNode in notifyNode.Nodes)
                {
                    ProcessChildNode(childNode, eventInvoker);
                }
            }
        }

    }
}